package com.bfl.learning.controller;

import com.bfl.learning.common.controller.BaseController;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.util.HashSet;
import java.util.Set;


@Controller
public class TestController extends BaseController {

    /**
     * 存储已有参数，用于判断参数是否重复，从而判断线程是否安全
     */
    private static Set<String> set = new HashSet<>();

    private String printAndReturn(HttpServletRequest request) {

        String value = request.getParameter("key");
        if (set.contains(value)) {
            System.out.println(value + "\t重复出现，request并发不安全！");
        } else {
            System.out.println(value);
            set.add(value);
        }

        return value;
    }

    /**
     * 方法1：Controller中加参数
     *
     * 分析：此时request对象是方法参数，相当于局部变量，毫无疑问是线程安全的。
     * 优缺点
     * 1)  如果多个controller方法中都需要request对象，那么在每个方法中都需要添加一遍request参数
     * 2)  request对象的获取只能从controller开始，如果使用request对象的地方在函数调用层级比较深的地方，那么整个调用链上的所有方法都需要添加request参数
     */
    @RequestMapping("/test1")
    @ResponseBody
    public String test1(HttpServletRequest request1) throws InterruptedException {
        // 判断线程安全
        String value = printAndReturn(request1);

        // 模拟程序执行了一段时间
        Thread.sleep(1000);

        return value;
    }

    /**
     * 方法2：自动注入
     *
     * 分析：在Spring中，Controller的scope是singleton(单例)，也就是说在整个web系统中，只有一个TestController；
     *      但是其中注入的request却是线程安全的，原因在于：使用这种方式，当Bean（本例的TestController）初始化时，
     *      Spring并没有注入一个request对象，而是注入了一个代理（proxy）；当Bean中需要使用request对象时，通过该代理获取request对象。
     *      AutowireUtils -> ObjectFactoryDelegatingInvocationHandler -> RequestObjectFactory.getObject() -> RequestContextHolder.currentRequestAttributes()
     *      通过这段代码可以看出，生成的RequestAttributes对象是线程局部变量（ThreadLocal），因此request对象也是线程局部变量；这就保证了request对象的线程安全性。
     *
     * 优缺点：
     *      1)  注入不局限于Controller中：在方法1中，只能在Controller中加入request参数。而对于方法2，不仅可以在Controller中注入，还可以在任何Bean中注入，包括Service、Repository及普通的Bean。
     *      2)  注入的对象不限于request：除了注入request对象，该方法还可以注入其他scope为request或session的对象，如response对象、session对象等；并保证线程安全。
     *      3)  减少代码冗余：只需要在需要request对象的Bean中注入request对象，便可以在该Bean的各个方法中使用，与方法1相比大大减少了代码冗余。
     */
    @Autowired
    private HttpServletRequest request2;
    @RequestMapping("/test2")
    @ResponseBody
    public String test2() throws InterruptedException {
        // 判断线程安全
        String value = printAndReturn(request2);

        // 模拟程序执行了一段时间
        Thread.sleep(1000);

        return value;
    }

    /**
     * 方法3：基类中自动注入
     *
     * 分析：
     *      在理解了方法2的线程安全性的基础上，很容易理解方法3是线程安全的：
     *      当创建不同的派生类对象时，基类中的域（这里是注入的request）在不同的派生类对象中会占据不同的内存空间，
     *      也就是说将注入request的代码放在基类中对线程安全性没有任何影响；测试结果也证明了这一点。
     *
     * 优缺点：
     *      与方法2相比，避免了在不同的Controller中重复注入request；
     *      但是考虑到java只允许继承一个基类，所以如果Controller需要继承其他类时，该方法便不再好用。
     */
    @RequestMapping("/test3")
    @ResponseBody
    public String test3() throws InterruptedException {
        // 判断线程安全
        String value = printAndReturn(request3);

        // 模拟程序执行了一段时间
        Thread.sleep(1000);

        return value;
    }

    /**
     * 方法4：手动调用
     *
     * 分析：
     *      该方法与方法2（自动注入）类似，只不过方法2中通过自动注入实现，本方法通过手动方法调用实现。因此本方法也是线程安全的。
     *
     * 优缺点：
     *      可以在非Bean中直接获取。缺点：如果使用的地方较多，代码非常繁琐；因此可以与其他方法配合使用。
     */
    @RequestMapping("/test4")
    @ResponseBody
    public String test4() throws InterruptedException {
        // 判断线程安全
        String value = printAndReturn(((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest());

        // 模拟程序执行了一段时间
        Thread.sleep(1000);

        return value;
    }

    /**
     * 方法5：@ModelAttribute方法
     */
    private HttpServletRequest request5;
    @ModelAttribute
    public void bindRequest(HttpServletRequest request5) {
        this.request5 = request5;
    }
    @RequestMapping("/test5")
    @ResponseBody
    public String test5() throws InterruptedException {
        // 判断线程安全
        String value = printAndReturn(request5);

        // 模拟程序执行了一段时间
        Thread.sleep(1000);

        return value;
    }

}
